route.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { NextRequest, NextResponse } from "next/server";
  2. import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
  3. import { getServerSideConfig } from "@/app/config/server";
  4. const config = getServerSideConfig();
  5. const mergedAllowedWebDavEndpoints = [
  6. ...internalAllowedWebDavEndpoints,
  7. ...config.allowedWebDevEndpoints,
  8. ].filter((domain) => Boolean(domain.trim()));
  9. const normalizeUrl = (url: string) => {
  10. try {
  11. return new URL(url);
  12. } catch (err) {
  13. return null;
  14. }
  15. };
  16. async function handle(
  17. req: NextRequest,
  18. { params }: { params: { path: string[] } },
  19. ) {
  20. if (req.method === "OPTIONS") {
  21. return NextResponse.json({ body: "OK" }, { status: 200 });
  22. }
  23. const folder = STORAGE_KEY;
  24. const fileName = `${folder}/backup.json`;
  25. const requestUrl = new URL(req.url);
  26. let endpoint = requestUrl.searchParams.get("endpoint");
  27. // Validate the endpoint to prevent potential SSRF attacks
  28. if (
  29. !endpoint ||
  30. !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
  31. const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
  32. const normalizedEndpoint = normalizeUrl(endpoint as string);
  33. return (
  34. normalizedEndpoint &&
  35. normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
  36. normalizedEndpoint.pathname.startsWith(
  37. normalizedAllowedEndpoint.pathname,
  38. )
  39. );
  40. })
  41. ) {
  42. return NextResponse.json(
  43. {
  44. error: true,
  45. msg: "Invalid endpoint",
  46. },
  47. {
  48. status: 400,
  49. },
  50. );
  51. }
  52. if (!endpoint?.endsWith("/")) {
  53. endpoint += "/";
  54. }
  55. const endpointPath = params.path.join("/");
  56. const targetPath = `${endpoint}${endpointPath}`;
  57. // only allow MKCOL, GET, PUT
  58. if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
  59. return NextResponse.json(
  60. {
  61. error: true,
  62. msg: "you are not allowed to request " + targetPath,
  63. },
  64. {
  65. status: 403,
  66. },
  67. );
  68. }
  69. // for MKCOL request, only allow request ${folder}
  70. if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
  71. return NextResponse.json(
  72. {
  73. error: true,
  74. msg: "you are not allowed to request " + targetPath,
  75. },
  76. {
  77. status: 403,
  78. },
  79. );
  80. }
  81. // for GET request, only allow request ending with fileName
  82. if (req.method === "GET" && !targetPath.endsWith(fileName)) {
  83. return NextResponse.json(
  84. {
  85. error: true,
  86. msg: "you are not allowed to request " + targetPath,
  87. },
  88. {
  89. status: 403,
  90. },
  91. );
  92. }
  93. // for PUT request, only allow request ending with fileName
  94. if (req.method === "PUT" && !targetPath.endsWith(fileName)) {
  95. return NextResponse.json(
  96. {
  97. error: true,
  98. msg: "you are not allowed to request " + targetPath,
  99. },
  100. {
  101. status: 403,
  102. },
  103. );
  104. }
  105. const targetUrl = targetPath;
  106. const method = req.method;
  107. const shouldNotHaveBody = ["get", "head"].includes(
  108. method?.toLowerCase() ?? "",
  109. );
  110. const fetchOptions: RequestInit = {
  111. headers: {
  112. authorization: req.headers.get("authorization") ?? "",
  113. },
  114. body: shouldNotHaveBody ? null : req.body,
  115. redirect: "manual",
  116. method,
  117. // @ts-ignore
  118. duplex: "half",
  119. };
  120. let fetchResult;
  121. try {
  122. fetchResult = await fetch(targetUrl, fetchOptions);
  123. } finally {
  124. console.log(
  125. "[Any Proxy]",
  126. targetUrl,
  127. {
  128. method: req.method,
  129. },
  130. {
  131. status: fetchResult?.status,
  132. statusText: fetchResult?.statusText,
  133. },
  134. );
  135. }
  136. return fetchResult;
  137. }
  138. export const PUT = handle;
  139. export const GET = handle;
  140. export const OPTIONS = handle;
  141. export const runtime = "edge";